/*
 * Copyright (c) 2009, Katholieke Universiteit Leuven
 *
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 
 *     * Redistributions of source code must retain the above copyright notice,
 *       this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright notice,
 *       this list of conditions and the following disclaimer in the documentation
 *       and/or other materials provided with the distribution.
 *     * Neither the name of the Katholieke Universiteit Leuven nor the names of
 *       its contributors may be used to endorse or promote products derived from
 *       this software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package be.kuleuven.cs.distrinet.wsn.looci.networkmanager;


import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import be.kuleuven.cs.distrinet.wsn.looci.event.Event;
import be.kuleuven.cs.distrinet.wsn.looci.event.EventManager;
import be.kuleuven.cs.distrinet.wsn.looci.event.IEventManager;
import be.kuleuven.cs.distrinet.wsn.looci.event.IEventSubscriber;
import be.kuleuven.cs.distrinet.wsn.looci.network.NetworkingFramework;
import be.kuleuven.cs.distrinet.wsn.looci.sunspot.runtime.network.SunSPOTNetworkingFramework;
import be.kuleuven.cs.distrinet.wsn.looci.util.ComponentTypes;
import be.kuleuven.cs.distrinet.wsn.looci.util.EventTypes;
import be.kuleuven.cs.distrinet.wsn.looci.util.PayloadBuilder;

import com.sun.spot.client.SpotClientCommands;
import com.sun.spot.peripheral.ota.ISpotAdminConstants;


/**
 * The NetworkManager is a backend maintenance application which has responsibility over
 * the connected WSN. It allows (un)deployment and starting and stopping of
 * LooCI components on the WSN.
 * It is furthermore accessible via a web service interface at the following URL:
 *      http://localhost:8080/LooCINetworkManagerWS
 *
 * Note: this is still in a preliminary stadium and only provides limited functionality.
 *
 * @author wouter, klaas
 *
 */
public class SpotClientNetworkManager implements ISingleNodeNetworkManager, IEventSubscriber {

    private static final long serialVersionUID = 1L;

    // Specify the directory in this project in which the suites will be stored
    public static final String SUITE_DIR = "suite";
    // Specify the directory in this project in which the components will be stored
    public static final String COMPONENTS_DIR = "components";
    private String appPath = null;
    private String libPath = null;
    private File armDirFile = null;
    private String keyStorePath = null;
    private String baseStationPort = null;
    private IEventManager eventManager;

    // Making everything quiescent
    private boolean makeQuiescent = false;
    private Map<String, byte[]> madeQuiescent = new HashMap<String, byte[]>();

    public SpotClientNetworkManager() {
        String userHome = System.getProperty("user.home");
        String SDKHome = System.getProperty("sunspot.home");
        String libName = System.getProperty("spot.library.name");

        appPath = SDKHome + File.separator + "dummy";
        libPath = SDKHome + File.separator + "arm" + File.separator + libName;
        armDirFile = new File(SDKHome, "arm");
        keyStorePath = userHome + File.separator + "sunspotkeystore";
        baseStationPort = System.getProperty("SERIAL_PORT");

        // Initialize LooCI event manager
        NetworkingFramework networkframework = SunSPOTNetworkingFramework.getInstance();
        eventManager = new EventManager(networkframework);

        // Making everything quiescent
        makeQuiescent = Boolean.parseBoolean(System.getProperty("networkmanager.makequiescent"));
        if (makeQuiescent) { // only subscribe if we will be doing something with it.
            System.out.println("PUTTING NODES INTO QUIESCENT MODE BEFORE DEPLOYING");
            eventManager.addLocalSubscriber(this);
        }

        // Use the following for testing purposes (see inner class code below)
//        TestThread testThread = new TestThread(this);
//        testThread.start();
    }

    public static void main(String[] args) {


    }

    /**
     * This method gets the .jar of the component specified by componentName out
     * of the components directory of this project and then uses the Romizer to
     * convert it to a suite which is stored in the suite directory.
     *
     * @param componentName
     * @return
     */
    private boolean convertJarToSuite(final String componentName) {
        String suiteloc = SUITE_DIR + File.separator + componentName;
        String suitejar = COMPONENTS_DIR + File.separator + componentName + ".jar";
        List<String> command = new ArrayList<String>();
        // the command
        command.add("java");
        command.add("-classpath");
        System.out.println("Romizer classpath: " + System.getProperty("romizer.classpath"));
        command.add(System.getProperty("romizer.classpath"));
        command.add("com.sun.squawk.Romizer");
        // romizer arguments
        command.add("-nobuildproperties");
        command.add("-suitepath:" + armDirFile.getAbsolutePath());
        command.add("-boot:squawk");
        command.add("-parent:" + System.getProperty("spot.library.name"));
        command.add("-metadata");
        command.add("-lnt");
        command.add("-strip:a");
        command.add("-cp:" + suitejar);
        command.add("-endian:little");
        command.add("-o:" + suiteloc);
        command.add(suitejar);
        try {
            (new File(SUITE_DIR)).mkdirs();
            System.out.println("[SunSPOT] Invoking romizer: ");
            for (String arg : command) {
                System.out.println("[SunSPOT] " + arg);
            }
            ProcessBuilder processBuilder = new ProcessBuilder("java");
            processBuilder.redirectErrorStream(true);
            Process romizer = processBuilder.command(command).start();
            BufferedReader romizerout = new BufferedReader(new InputStreamReader(romizer.getInputStream()));
            String line;
            while ((line = romizerout.readLine()) != null) {
                System.out.println(line);
            }
            return (romizer.waitFor() == 0);
        } catch (InterruptedException e) {
            return false;
        } catch (IOException e) {
            return false;
        }
    }

    /**
     * Deploy the named component to the specified SunSPOT.
     *
     * @param targetAddress
     * @param componentName
     * @return
     */
    public boolean deployComponent(String targetAddress, String componentName) {
        try {
            if (!convertJarToSuite(componentName)) {
                return false;
            }
            System.out.println("finished convertJarToSuite");
            makeNodeQuiescent(targetAddress);
            File suiteDir = new File(SUITE_DIR);
            SpotClientCommands scc = new SpotClientCommands(new SimplePrintUI(), appPath, libPath, armDirFile, keyStorePath, baseStationPort, targetAddress, ISpotAdminConstants.MASTER_ISOLATE_ECHO_PORT);
            System.out.println("start synchronize");
            scc.execute("synchronize");
            System.out.println("start flashapp");
            scc.execute("flashapp", "false", "0", "spotsuite://" + componentName, suiteDir.getCanonicalPath() + File.separator + componentName);
            System.out.println("start quit");
            scc.execute("quit");
            makeNodeUnQuiescent(targetAddress);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * Undeploy the named component at the specified SunSPOT.
     *
     * @param targetAddress
     * @param componentName
     * @return
     */
    public boolean undeployComponent(String targetAddress, String componentName) {
        try {
            SpotClientCommands scc = new SpotClientCommands(new SimplePrintUI(), appPath, libPath, armDirFile, keyStorePath, baseStationPort, targetAddress, ISpotAdminConstants.MASTER_ISOLATE_ECHO_PORT);
            scc.execute("synchronize");
            scc.execute("undeploy", "spotsuite://" + componentName);
            scc.execute("quit");
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * Start the named component at the specified SunSPOT.
     *
     * @param targetAddress
     * @param componentName
     * @return
     */
    public boolean startComponent(String targetAddress, String componentName) {
        try {
            SpotClientCommands scc = new SpotClientCommands(new SimplePrintUI(), appPath, libPath, armDirFile, keyStorePath, baseStationPort, targetAddress, ISpotAdminConstants.MASTER_ISOLATE_ECHO_PORT);
            scc.execute("synchronize");
            scc.execute("startapp", "spotsuite://" + componentName, "1");
            scc.execute("quit");
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * Stop the named component at the specified SunSPOT.
     *
     * @param ta
     * @param cn
     * @return
     */
    public boolean stopComponent(String targetAddress, String componentName) {
// Can't this be implemented using the stopapp command? Yes, but it requires to get the isolateId
// using the GetAllAppsStatusCmd and parsing the result... Anyway it might be better to do this the
// LooCI way.
//        try {
//            SpotClientCommands scc = new SpotClientCommands(new SimplePrintUI(), appPath, libPath, armDirFile, keyStorePath, baseStationPort, targetAddress, ISpotAdminConstants.MASTER_ISOLATE_ECHO_PORT);
//            scc.execute("synchronize");
//            scc.execute("stopapp", "be.kuleuven.cs.distrinet.wsn.looci.components.LightSensor#2@0014.4F01.0000.55E5");
//            scc.execute("quit");
//            return true;
//        } catch (Exception e) {
//            e.printStackTrace();
//            return false;
//        }

        byte componentId = getComponentId(componentName);
        PayloadBuilder payload = new PayloadBuilder();
        payload.addByte(componentId);
        Event event = new Event(EventTypes.COMPONENTSTOP, payload.getPayload());
        event.setRemoteDestination(targetAddress);
        eventManager.publishEvent(event);
        // TODO is there a way to detect whether this succeeded ?
        return true;
    }

    /**
     * Puts all the LooCI components at the specified node in quiescent mode.
     *
     * @param targetAddress
     */
    private void makeNodeQuiescent(final String targetAddress) {
        if (makeQuiescent) {
            System.out.println("Putting node " + targetAddress + " into quiescent mode.");
            PayloadBuilder payload = new PayloadBuilder();
            Event maqEvent = new Event(EventTypes.MAKEALLQUIESCENT, payload.getPayload());
            maqEvent.setRemoteDestination(targetAddress);
            eventManager.publishEvent(maqEvent);
//			FIXME: calling wait more than once in the same thread seems to lock the whole system 
//			try {
//				synchronized(madeQuiescent) {
//					// wait for the returned event, for a max. of 5 seconds
//					System.out.println("We're in thread " + Thread.currentThread().getId());
//					madeQuiescent.wait();
//					System.out.println("We're back!");
//				}
//			} catch (InterruptedException e) {
//				e.printStackTrace();
//			}
            System.out.println("Thread id in makeNodeQuiescent: " + Thread.currentThread().getId());
        }
    }

    /**
     * Restarts all LooCI components at the specified node.
     *
     * @param targetAddress
     */
    private void makeNodeUnQuiescent(String targetAddress) {
        if (makeQuiescent) {
            System.out.println("Restarting node " + targetAddress + " from quiescent mode.");
//			synchronized(madeQuiescent) {
//				if(!madeQuiescent.containsKey(targetAddress.toUpperCase())) {
//					try {
//						// wait another 5 seconds for the return event
//						System.out.println("Waiting for the made all quiescent event");
//						madeQuiescent.wait(5000);
//					} catch (InterruptedException e) {
//						e.printStackTrace();
//					}					
//				}
//			}
            PayloadBuilder payload = new PayloadBuilder();
            if (madeQuiescent.containsKey(targetAddress.toUpperCase())) {
                byte[] torestart = (byte[]) madeQuiescent.get(targetAddress.toUpperCase());
                for (int i = 0; i < torestart.length; ++i) {
                    System.out.println("Adding component id " + torestart[i] + " to list for restart");
                    payload.addByte(torestart[i]);
                }
            }
            Event saEvent = new Event(EventTypes.STARTALL, payload.getPayload());
            saEvent.setRemoteDestination(targetAddress);
            eventManager.publishEvent(saEvent);
        }
    }

    private byte getComponentId(String cn) {
        // FIXME: this should be extensible
        if (cn.equals("TestApplication")) {
            return ComponentTypes.TEST_APPLICATION;
        } else if (cn.equals("BlinkBlue")) {
            return ComponentTypes.BLINK_BLUE;
        } else if (cn.equals("BlinkRed")) {
            return ComponentTypes.BLINK_RED;
        } else if (cn.equals("LightSensor_1.0.0")) {
            return ComponentTypes.LIGHT_SENSOR;
        } else if (cn.equals("TempSensor")) {
            return 10;
        } else {
            // FIXME: just return 0
            return 0;
        }
    }

    public byte[] getSubscriptions() {
        return new byte[]{EventTypes.MADEALLQUIESCENT, EventTypes.STARTEDALL};
    }

    public void receiveEvent(Event e) {
        System.out.println("Thread id in receiveEvent: " + Thread.currentThread().getId());
        if (makeQuiescent) {
            // only do something with these events if we have issued commands
            // as a matter of fact, we should only get events if this is true, but well, a sanity check never hurts
            if (e.getType() == EventTypes.MADEALLQUIESCENT) {
                System.out.println("Received made all quiescent event from " + e.getSource());
                PayloadBuilder payload = new PayloadBuilder(e.getPayload());
                byte[] quiescent = new byte[payload.getNbElements()];
                for (int i = 0; i < payload.getNbElements(); ++i) {
                    quiescent[i] = payload.getByteAt(i);
                }
                synchronized (madeQuiescent) {
                    madeQuiescent.put(e.getSource().toUpperCase(), quiescent);
                    madeQuiescent.notifyAll();
                }
            }
        }
    }

//    private class TestThread extends Thread {
//
//        private ISingleNodeNetworkManager nm;
//
//        public TestThread(ISingleNodeNetworkManager nm) {
//            this.nm = nm;
//        }
//
//        public void run() {
//            String node = "0014.4F01.0000.55E5";
//            String app = "LightSensor_1.0.0";
//
//            System.out.println("TRYING TO DEPLOY");
//
//            long start = System.currentTimeMillis();
//            if (nm.deployComponent(node, app)) {
//                System.out.println("SUCCESS - DEPLOYED!");
//            } else {
//                System.out.println("FAIL - NOT DEPLOYED!");
//            }
//
//            System.out.println("TRYING TO START");
//            if (nm.startComponent(node, app)) {
//                System.out.println("SUCCESS - STARTED!");
//            } else {
//                System.out.println("FAIL - NOT STARTED!");
//            }
//
//            long end = System.currentTimeMillis();
//
//            System.out.println("Deploying and starting took: " + (end - start) + " ms");
//
//            try {
//                Thread.sleep(10000);
//            } catch (InterruptedException e) {
//                e.printStackTrace();
//            }
//
//            System.out.println("TRYING TO STOP");
//            if(nm.stopComponent(node, app)) {
//                System.out.println("SUCCESS - STOPPED!");
//            } else {
//                System.out.println("FAIL - NOT STOPPED!");
//            }
//
//            System.out.println("TRYING TO UNDEPLOY");
//            if(nm.undeployComponent(node, app)) {
//                System.out.println("SUCCESS - UNDEPLOYED!");
//            } else {
//                System.out.println("FAIL - NOT UNDEPLOYED!");
//            }
//
//            System.out.println("DEPLOYING BLINK BLUE");
//
//            System.out.println("TRYING TO DEPLOY");
//            if (nm.deployComponent(node, "BlinkBlue")) {
//                System.out.println("SUCCESS - DEPLOYED!");
//            } else {
//                System.out.println("FAIL - NOT DEPLOYED!");
//            }
//
//            System.out.println("STARTING BLINK BLUE");
//            System.out.println("TRYING TO START");
//            if (nm.startComponent(node, "BlinkBlue")) {
//                System.out.println("SUCCESS - STARTED!");
//            } else {
//                System.out.println("FAIL - NOT STARTED!");
//            }
//
//            System.out.println("SLEEPING 10 SECONDS");
//            try {
//                Thread.sleep(10000);
//            } catch (InterruptedException e) {
//                e.printStackTrace();
//            }
//
//            System.out.println("DEPLOYING BLINK RED");
//            System.out.println("TRYING TO DEPLOY");
//            if (nm.deployComponent(node, "BlinkRed")) {
//                System.out.println("SUCCESS - DEPLOYED!");
//            } else {
//                System.out.println("FAIL - NOT DEPLOYED!");
//            }
//
//            System.out.println("STARTING BLINK RED");
//            System.out.println("TRYING TO START");
//            if (nm.startComponent(node, "BlinkRed")) {
//                System.out.println("SUCCESS - STARTED!");
//            } else {
//                System.out.println("FAIL - NOT STARTED!");
//            }
//        }
//    }
}


